#!/bin/env python

"""
Generic data plotter. DOCS NEED UPDATING...

Much thanks goes to Gordon Williams the author of the wx.lib.plot
demo, published under the WX license.

To make use of plotter in your application, import this module then
create a new class to configure your plots and unmarshall the data
coming in through a socket -- plotter will be listening for data on
port 8081 on localhost. Your class should set a number of instance
variables (see the example below) and should also provide a method
called unmarshall which should accept a string argument (a datum
received from the socket) and return an unmarshalled 2-tuple of
floats. This will be plotted on the chart so you can think of the
2-tuple as an (x,y) point on the plotted curve.

### Example use of the plotter module (Currently bollox, docs need
updating)

import plotter

class MyUnmarshaller(object):
    	def __init__(self, ...):
            	self.title = ...        # Plot title (str)
		self.max_data = ...     # Max datapoints on x-axis (int)
                self.legend = ...       # Plot legend (str)
                self.xlabel = ...       # X-axis label (str)
                self.ylabel = ...	# Y-axis label (str)
                self.colour = ...   	# Marker colour (str)
                self.marker = ...     	# Marker type (str --see below)
                self.width = ...	# Marker width (int)
                self.offset_d = ...     # Data offsets (dict)
                self.offset = ...   	# Current offset (float 2-tuple)
                self.isLine = ...  	# Plot lines or points (bool)
                return
	def unmarshall(self, string):
            	\"""Take a string and return a 2-tulple of floats.\"""
            	...
            	x = ...
            	y = ...
            	return (x,y)

if __main__ == '__name__':
    	# Run GUI
    	plotter.PlottingApp.unmarshaller = MyUnmarshaller()
    	plotter.PlottingApp(0).MainLoop()

### EOF

Possible marker shapes:
        - 'circle'
        - 'dot'
        - 'square'
        - 'triangle'
        - 'triangle_down'
        - 'cross'
        - 'plus'
"""

import copy, socket, time, thread, threading, wx
from wx.lib.newevent import *
from wx.lib.plot import *

DEBUG = False

__author__  = 'Sarah Mount <s.mount@coventry.ac.uk>'
__credits__ = 'Gordon Williams -- wx.lib.plot demo'
__date__    = 'March 2007'

### Socket setup
localhost = socket.gethostname()	# Localhost
port = 8081				# Port number for sockets
buffsize = 1024				# Buffer size for sockets

### Plotting
din = False				# Set if we're plotting data
blank_data = {'Sensor0':[(0.0,0.0)]}				# Also used by OnClear()
data = copy.copy(blank_data)		# Global to store plot data
data_l = threading.Lock()		# Lock for the data variable

### PlotMe Event -- note you can't update the GUI from a separate thread!
PlotMe, EVT_PLOT_ME = NewEvent()

### Plotting app. This is the only thing you need to instantiate to use
### this module.
class PlottingApp(wx.App):
    """Make sure you use Plotting.App.unmarshaller before calling MainLoop."""
    def OnInit(self):
    	wx.VisualAttributes.colBG = wx.NamedColour('white')
    	wx.InitAllImageHandlers()
    	frame = GenericPlotter(None, -1, PlottingApp.unmarshaller)
    	frame.Show(True)
    	self.SetTopWindow(frame)
    	return True


class Unmarshaller(object):
    	"""Should be subclassed in client code."""
    	def __init__(self,
		     offset_d,
		     offset,
		     isLine=False):
                self.isLine = True		# Plot lines (or points)
            	self.title = 'Data vs. Time'	# Plot title
		self.max_data = 10		# Max datapoints
                self.xlabel = 'Time(secs)'	# X-axis label
                self.ylabel = 'Data'		# Y-axis label
                self.offset_d = offset_d	# Data offsets
                self.offset = offset		# Current offset
		self.config = {'data':plotter.SeriesConfig(legend='data')}
		return
	def unmarshall(self, s):
	   	"""Unmarshall string data coming from the socket. Should be overridden."""
	    	return s


class SeriesConfig(object):
    	"""Used by client code to configure plots."""
    	def __init__(self,
		     legend='mote data',
		     colour='black',
		     marker='cross',
                     width=2):
                self.legend = legend     	# Plot legend
                self.colour = colour		# Marker colour
                self.marker = marker 		# Marker type
                self.width = width 		# Marker width
		return


def createData():
	"""Return an object to place on a plotting canvas."""
	markers = []
    	data_l.acquire()
        if PlottingApp.unmarshaller.isLine:
            for key in data:
		markers.append(PolyLine(data[key][-PlottingApp.unmarshaller.max_data:],
                                   	legend=PlottingApp.unmarshaller.config[key].legend,
	        	           	colour=PlottingApp.unmarshaller.config[key].colour,
                                   	width=PlottingApp.unmarshaller.config[key].width))
        else:
            for key in data:
    		markers.append(PolyMarker(data[key][-PlottingApp.unmarshaller.max_data:],
        				  legend=PlottingApp.unmarshaller.config[key].legend,
        	        	          colour=PlottingApp.unmarshaller.config[key].colour,
                                	  width=PlottingApp.unmarshaller.config[key].width,
                                	  marker=PlottingApp.unmarshaller.config[key].marker))

    	data_l.release()
    	return PlotGraphics(markers,
    			    PlottingApp.unmarshaller.title,
    			    PlottingApp.unmarshaller.xlabel,
                   	    PlottingApp.unmarshaller.ylabel)


def socketlistener(plotter):
	"""Listen on a UDP socket for accel data and redraw the plot."""
	sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
	sock.bind(('', port))
        global din
	while True:
    		accel, addr = sock.recvfrom(buffsize)
                if accel and din:
                        mote, x, y = PlottingApp.unmarshaller.unmarshall(accel)
                        if not mote:
                            continue
                        data_l.acquire()
                        print mote, x, y
                        if mote in data:
				data[mote].append((x,y))
                        else:
                            	data[mote] = [(x,y)]
                        data_l.release()
                        evt = PlotMe(points=createData())
                        wx.PostEvent(plotter, evt)
    	return

class GenericPlotter(wx.Frame):
    def __init__(self, parent, id, unmarshaller):
        self.unmarshaller = unmarshaller
        wx.Frame.__init__(self, parent, id, self.unmarshaller.title,
                          wx.DefaultPosition, size=(800, 600))
	# Create GUI
        self._create_gui()

	# Configure plot canvas
        self.client.SetEnableGrid(True)
        self.client.SetEnableLegend(True)
	self.OnPlotDraw(True)

	# Configure 'checked' menu items
        self.mainmenu.Check(215, self.client.GetEnableGrid())
    	self.mainmenu.Check(220, self.client.GetEnableLegend())
    	self.mainmenu.Check(222, self.client.GetEnablePointLabel())
    	self.mainmenu.Check(214, self.client.GetEnableZoom())

        # Bind the custom PlotMe event to the GUI
	self.Bind(EVT_PLOT_ME, self.OnPlotMe)
	EVT_PLOT_ME(self, self.OnPlotMe)

	# Listen out for mote data
	self.socketlistener = thread.start_new_thread(socketlistener, (self,))

	# Display GUI
        self.Show(True)
	return

    def OnPlotMe(self, evt):
        """Called from the thread listening on the motes. Passes PlotGraphics
        object to be drawn on the plot canvas.
        """
        self.client.Draw(evt.points)
        return

    def OnRadioBox(self, event):
        """Calibrate data againts a different mote."""
        offset = self.unmarshaller.offset_d['mote'+str(event.GetInt())]
        self.GetStatusBar().SetStatusText('Calibrating for mote'+
					  str(event.GetInt())+
                                          '. Offsets: '+str(offset))
        return
    
    def OnOK(self, evt):
        """Start plotting live data."""
        global din
        din = True
        self.GetStatusBar().SetStatusText('Plotting mote data...')
        return
    
    def OnCancel(self, evt):
        """Stop plotting data."""
        global din
        din = False
        self.GetStatusBar().SetStatusText('Plotting cancelled.')
        return
    
    def _create_gui(self):
        # Now Create the menu bar and items
        self.mainmenu = wx.MenuBar()
	# File menu
        menu = wx.Menu()
        menu.Append(200, 'Page Setup...', 'Setup the printer page')
        self.Bind(wx.EVT_MENU, self.OnFilePageSetup, id=200)

        menu.Append(201, 'Print Preview...', 'Show the current plot on page')
        self.Bind(wx.EVT_MENU, self.OnFilePrintPreview, id=201)

        menu.Append(202, 'Print...', 'Print the current plot')
        self.Bind(wx.EVT_MENU, self.OnFilePrint, id=202)

        menu.Append(203, 'Save Plot...', 'Save current plot')
        self.Bind(wx.EVT_MENU, self.OnSaveFile, id=203)

        menu.Append(205, 'E&xit', 'Enough of this already!')
        self.Bind(wx.EVT_MENU, self.OnFileExit, id=205)
        self.mainmenu.Append(menu, '&File')
	# Plot menu
        menu = wx.Menu()
    	menu.Append(206, 'Plot', 'Plot data')
        self.Bind(wx.EVT_MENU,self.OnPlotDraw, id=206)
        menu.Append(211, '&Redraw', 'Redraw plots')
        self.Bind(wx.EVT_MENU,self.OnPlotRedraw, id=211)
        menu.Append(212, '&Delete plot', 'Delete canvas')
        self.Bind(wx.EVT_MENU,self.OnPlotClear, id=212)
        menu.Append(213, '&Scale', 'Scale canvas')
        self.Bind(wx.EVT_MENU,self.OnPlotScale, id=213)
        menu.Append(214, 'Enable &Zoom', 'Enable Mouse Zoom',
                    kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU,self.OnEnableZoom, id=214)
        menu.Append(215, 'Enable &Grid', 'Turn on Grid', kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU,self.OnEnableGrid, id=215)
        menu.Append(220, 'Enable &Legend', 'Turn on Legend',
                    kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU,self.OnEnableLegend, id=220)
        menu.Append(222, 'Enable &Point Label', 'Show Closest Point',
                    kind=wx.ITEM_CHECK)
        self.Bind(wx.EVT_MENU,self.OnEnablePointLabel, id=222)
        menu.Append(225, 'Scroll Up 1', 'Move View Up 1 Unit')
        self.Bind(wx.EVT_MENU,self.OnScrUp, id=225)
        menu.Append(230, 'Scroll Rt 2', 'Move View Right 1 Unit')
        self.Bind(wx.EVT_MENU,self.OnScrRt, id=230)
        menu.Append(235, '&Reset zoom', 'Reset zoom')
        self.Bind(wx.EVT_MENU,self.OnReset, id=235)
        self.mainmenu.Append(menu, '&Plot')
	# Help menu
        menu = wx.Menu()
        menu.Append(300, '&About', 'About this app...')
        self.Bind(wx.EVT_MENU, self.OnHelpAbout, id=300)
        self.mainmenu.Append(menu, '&Help')
        self.SetMenuBar(self.mainmenu)

        # A status bar to tell people what's happening
        self.CreateStatusBar(1)

        # Splitters....
    	splitter = wx.SplitterWindow(self, -1)
        def EmptyHandler(evt): pass
        splitter.Bind(wx.EVT_ERASE_BACKGROUND, EmptyHandler)

	# Button panel
	self.controls = wx.Panel(splitter, size=(800, 47))

	# Start button
    	self.ok_b = wx.Button(self.controls, wx.ID_OK, "OK", (5, 10))
        self.Bind(wx.EVT_BUTTON, self.OnOK, self.ok_b)
        self.ok_b.SetDefault()
        self.ok_b.SetSize(self.ok_b.GetBestSize())

	# Stop button
    	self.cancel_b = wx.Button(self.controls, wx.ID_CANCEL,
                                  "Cancel", (105, 10))
        self.Bind(wx.EVT_BUTTON, self.OnCancel, self.cancel_b)
        self.cancel_b.SetDefault()
        self.cancel_b.SetSize(self.cancel_b.GetBestSize())

	# Clear button
    	self.clear_b = wx.Button(self.controls, wx.ID_CLEAR,
                                 "Clear", (205, 10))
        self.Bind(wx.EVT_BUTTON, self.OnClear, self.clear_b)
        self.clear_b.SetDefault()
        self.clear_b.SetSize(self.clear_b.GetBestSize())

	# Choose calibration settings
        self.rb = wx.RadioBox(self.controls, -1, "Mote calibration",
                              (310, 2), wx.DefaultSize,
                              self.unmarshaller.offset_d.keys(),
                              1, style=wx.RA_VERTICAL | wx.NO_BORDER
                              )
 	self.Bind(wx.EVT_RADIOBOX, self.OnRadioBox, self.rb)

	# Plot canvas
        self.client = PlotCanvas(splitter)

        # Define the function for drawing pointLabels
        self.client.SetPointLabelFunc(self.DrawPointLabel)
        # Create mouse event for showing cursor coords in status bar
        self.client.Bind(wx.EVT_LEFT_DOWN, self.OnMouseLeftDown)
        # Show closest point when enabled
        self.client.Bind(wx.EVT_MOTION, self.OnMotion)

	# Configure splitter window
	splitter.SetSashGravity(0.0)
       	splitter.SplitHorizontally(self.controls, self.client, sashPosition=47)
	splitter.UpdateSize()
        return

    def OnEnableScroll(self, event):
        self.client.SetShowScrollbars(eventIsChecked())
        return

    def OnClear(self, evt):
        global data
	data_l.acquire()
        data = blank_data[:]
        data_l.release()
        self.client.Draw(createData())
        return

    def DrawPointLabel(self, dc, mDataDict):
        """This is the fuction that defines how the pointLabels are plotted
            dc - DC that will be passed
            mDataDict - Dictionary of data that you want to use for the
            pointLabel.

            As an example I have decided I want a box at the curve point
            with some text information about the curve plotted below.
            Any wxDC method can be used.
        """
        # ----------
        dc.SetPen(wx.Pen(wx.BLACK))
        dc.SetBrush(wx.Brush( wx.BLACK, wx.SOLID ) )

        sx, sy = mDataDict["scaledXY"] #scaled x,y of closest point
        dc.DrawRectangle( sx-5,sy-5, 10, 10)  #10by10 square centered on point
        px,py = mDataDict["pointXY"]
        cNum = mDataDict["curveNum"]
        pntIn = mDataDict["pIndex"]
        legend = mDataDict["legend"]
        #make a string to display
        s = "Crv# %i, '%s', Pt. (%.2f,%.2f), PtInd %i" %(cNum, legend, px,
                                                         py, pntIn)
        dc.DrawText(s, sx , sy+1)
        # -----------

    def OnMouseLeftDown(self,event):
        s= "Left Mouse Down at Point: (%.4f, %.4f)" % self.client.GetXY(event)
        self.SetStatusText(s)
        event.Skip()            #allows plotCanvas OnMouseLeftDown to be called

    def OnMotion(self, event):
        #show closest point (when enbled)
        if self.client.GetEnablePointLabel() == True:
            #make up dict with info for the pointLabel
            #I've decided to mark the closest point on the closest curve
            dlst= self.client.GetClosetPoint(self.client.GetXY(event),
                                             pointScaled= True)
            if dlst != []:    #returns [] if none
                curveNum, legend, pIndex, pointXY, scaledXY, distance = dlst
                #make up dictionary to pass to my user function
                #(see DrawPointLabel)
                mDataDict= {"curveNum":curveNum, "legend":legend,
                            "pIndex":pIndex,\
                            "pointXY":pointXY, "scaledXY":scaledXY}
                #pass dict to update the pointLabel
                self.client.UpdatePointLabel(mDataDict)
        event.Skip()           #go to next handler

    def OnFilePageSetup(self, event):
        self.client.PageSetup()

    def OnFilePrintPreview(self, event):
        self.client.PrintPreview()

    def OnFilePrint(self, event):
        self.client.Printout()

    def OnSaveFile(self, event):
        self.client.SaveFile()

    def OnFileExit(self, event):
        self.Close()

    def OnPlotDraw(self, event):
        """Only called on startup now.
        OnPlotMe events are posted by the thread listening on the mote socket.
        """
        self.resetDefaults()
        self.client.SetFont(wx.Font(10,wx.NORMAL,wx.NORMAL,wx.NORMAL))
        self.client.SetFontSizeAxis(14)
        self.client.SetFontSizeLegend(12)
        self.client.SetXSpec('min')
        self.client.SetYSpec('min')
        self.client.Draw(createData())

    def OnPlotRedraw(self,event):
        self.client.Redraw()

    def OnPlotClear(self,event):
        self.client.Clear()

    def OnPlotScale(self, event):
        if self.client.last_draw != None:
            graphics, xAxis, yAxis= self.client.last_draw
            self.client.Draw(graphics,(1,3.05),(0,1))

    def OnEnableZoom(self, event):
        self.client.SetEnableZoom(event.IsChecked())

    def OnEnableGrid(self, event):
        self.client.SetEnableGrid(event.IsChecked())

    def OnEnableLegend(self, event):
        self.client.SetEnableLegend(event.IsChecked())

    def OnEnablePointLabel(self, event):
        self.client.SetEnablePointLabel(event.IsChecked())

    def OnScrUp(self, event):
        self.client.ScrollUp(1)

    def OnScrRt(self,event):
        self.client.ScrollRight(1)

    def OnReset(self,event):
        self.client.Reset()

    def OnHelpAbout(self, event):
        from wx.lib.dialogs import ScrolledMessageDialog
        about = ScrolledMessageDialog(self,
                                      'Plotting.\n\nCogent Computing', 'About...')
        about.ShowModal()

    def resetDefaults(self):
        """Just to reset the fonts back to the PlotCanvas defaults"""
        self.client.SetFont(wx.Font(10,wx.NORMAL,wx.NORMAL,wx.NORMAL))
        self.client.SetFontSizeAxis(10)
        self.client.SetFontSizeLegend(7)
        self.client.SetXSpec('min')
        self.client.SetYSpec('min')
